use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::thread::ThreadId;

use config::{Map, Value, ValueKind};
use lazy_static::lazy_static;
use log::{LevelFilter, Log, Metadata, Record};
use rand::Rng;
use tempfile::TempDir;

use crate::tiling::tiles::tile_list::TileList;
use crate::tiling::tiles::{Connectors, Tile, TileType};
use crate::tiling::{Direction, MissingTilePolicy, TileSelectionPolicy, VacancyPolicy};

lazy_static! {
    pub(crate) static ref RANDOM_USIZE: usize = rand_usize(0, 3);
    pub(crate) static ref RANDOM_USIZES: Vec<usize> =
        std::iter::repeat_with(|| rand_usize(0, 50)).take(10).collect();
    pub(crate) static ref RANDOM_USIZES_SMALL: Vec<usize> =
        std::iter::repeat_with(|| rand_usize(2, 5)).take(10).collect();
    pub(crate) static ref RANDOM_OFFSETS: Vec<(isize, isize)> =
        std::iter::repeat_with(rand_offset).take(10).collect();
    pub(crate) static ref RANDOM_F64: f64 = rand_float();
    pub(crate) static ref RANDOM_BOOL: bool = rand_bool();
    pub(crate) static ref RANDOM_COORDINATES: Vec<(usize, usize)> =
        std::iter::repeat_with(random_coordinates).take(10).collect();
    pub(crate) static ref RANDOM_TILES: Vec<Tile> = random_tiles(rand_usize(20, 50));
    pub(crate) static ref RANDOM_STRINGS: Vec<String> =
        (0..20).map(|x| rand_string(rand_usize(x * 5, x * 5 + 5))).collect();
    pub(crate) static ref RANDOM_STRINGS_TWO: Vec<String> =
        std::iter::repeat_with(|| rand_string(rand_usize(0, 10))).take(10).collect();
    pub(crate) static ref RANDOM_MISSING_TILE_POLICY: MissingTilePolicy =
        random_missing_tile_policy();
    pub(crate) static ref RANDOM_PREDECESSOR: Option<(usize, usize)> = random_predecessor();
    pub(crate) static ref RANDOM_BORDER_TILE_ID: Option<usize> = random_border_tile_id();
    pub(crate) static ref RANDOM_TILE_SELECTION_POLICY: TileSelectionPolicy =
        random_tile_selection_policy();
    pub(crate) static ref RANDOM_VACANCY_POLICY: VacancyPolicy = random_vacancy_policy();
    pub(crate) static ref RANDOM_TILE_TYPE: TileType = random_tile_type();
    pub(crate) static ref RANDOM_DIRECTION: Option<Direction> = random_option_direction();
    pub(crate) static ref RANDOM_CONNECTORS: Connectors = random_connectors();
    pub(crate) static ref RANDOM_TEXT_TILE_CONTENT_VECTOR: Vec<String> =
        random_text_tile_content_vector_of_size(RANDOM_USIZES[3] + 1, RANDOM_USIZES[7] + 1);
    pub(crate) static ref RANDOM_LAST_LEAVES: Vec<(usize, usize)> = random_last_leaves();
    pub(crate) static ref RANDOM_CONNECTORS_NORTH: Connectors = Connectors {
        north: rand_usize(2, 10),
        ..RANDOM_CONNECTORS.clone()
    };
    pub(crate) static ref RANDOM_CONNECTORS_SOUTH: Connectors = Connectors {
        south: rand_usize(2, 10),
        ..RANDOM_CONNECTORS.clone()
    };
    pub(crate) static ref RANDOM_CONNECTORS_EAST: Connectors = Connectors {
        east: rand_usize(2, 10),
        ..RANDOM_CONNECTORS.clone()
    };
    pub(crate) static ref RANDOM_CONNECTORS_EAST_WEST: Connectors = Connectors {
        north: rand_usize(1, 10),
        east: rand_usize(2, 10),
        south: 1,
        west: rand_usize(2, 10),
    };
    pub(crate) static ref RANDOM_CONNECTORS_ALL: Connectors = Connectors {
        north: rand_usize(2, 10),
        east: rand_usize(2, 10),
        south: rand_usize(2, 10),
        west: rand_usize(2, 10),
    };
    pub(crate) static ref RANDOM_EXPOSED_EDGES: Vec<Vec<Direction>> =
        std::iter::repeat_with(random_exposed_edges).take(10).collect();
    pub(crate) static ref RANDOM_EXCLUDED_CONNECTORS: Vec<HashSet<Connectors>> =
        std::iter::repeat_with(random_excluded_connectors).take(10).collect();
    pub(crate) static ref EXPECTED_EXCLUDED_CONNECTORS: Vec<HashSet<Connectors>> = vec![
        {
            let mut connectors = RANDOM_EXCLUDED_CONNECTORS[1].clone();
            connectors.insert(RANDOM_CONNECTORS_SOUTH.clone());
            connectors
        },
        {
            let mut connectors = RANDOM_EXCLUDED_CONNECTORS[1].clone();
            connectors.insert(RANDOM_CONNECTORS_NORTH.clone());
            connectors
        },
        {
            let mut connectors = RANDOM_EXCLUDED_CONNECTORS[1].clone();
            connectors.insert(RANDOM_CONNECTORS_EAST.clone());
            connectors
        },
    ];
    pub(crate) static ref TILE_CONNECTION_TILES: Vec<Tile> = vec![
        random_tile_text(Connectors { north: 1, east: 1, south: 1, west: 1 }),
        random_tile_text(Connectors { north: 1, east: 2, south: 2, west: 1 }),
        random_tile_text(Connectors { north: 1, east: 1, south: 2, west: 1 }),
        random_tile_text(Connectors { north: 2, east: 3, south: 1, west: 4 }),
        random_tile_text(Connectors { north: 3, east: 3, south: 2, west: 4 }),
    ];

    // Taken from https://stackoverflow.com/a/67433684
    pub(crate) static ref SERIAL_TEST: Mutex<()> = Mutex::default();

    pub(crate) static ref TEMPORARY_DIRECTORY: TempDir = make_temporary_directory();

    pub(crate) static ref TEMPORARY_DIRECTORY_TWO: TempDir = make_temporary_directory();

    pub(crate) static ref TEST_VALUE_KIND_VECTOR: Vec<ValueKind> = vec![
        ValueKind::Table(Map::new()),
        ValueKind::String("öß".to_string()),
        ValueKind::Array(vec![Value::new(Some(&RANDOM_STRINGS[6]), ValueKind::Nil)]),
        ValueKind::Boolean(false)
    ];

    pub(crate) static ref TEST_VALUE_VECTOR: Vec<Value> = vec![
        Value::new(Some(&RANDOM_STRINGS[2]), ValueKind::Table(Map::new())),
        Value::new(Some(&"".to_string()), ValueKind::String("öß".to_string())),
        Value::new(
            None, ValueKind::Array(vec![Value::new(Some(&RANDOM_STRINGS[6]), ValueKind::Nil)])
        ),
        Value::new(Some(&RANDOM_STRINGS[7]), ValueKind::Boolean(false))
    ];
    pub(crate) static ref TEST_VALUE_KIND_ARRAY: ValueKind =
        ValueKind::Array(TEST_VALUE_VECTOR.clone());

    pub(crate) static ref TEST_LOGGER: TestLogger = TestLogger {
        messages: Mutex::new(HashMap::new()),
    };
}

pub(crate) struct TestLogger {
    pub messages: Mutex<HashMap<ThreadId, Vec<String>>>,
}

impl Log for TestLogger {
    fn enabled(&self, _: &Metadata) -> bool {
        true
    }

    fn log(&self, record: &Record) {
        let mut messages = self.messages.lock().unwrap_or_else(|e| e.into_inner());
        let thread_messages = messages.entry(std::thread::current().id()).or_default();
        thread_messages.push(format!(
            "{}: {} ⸙ {}",
            record.target(),
            record.level(),
            record.args(),
        ));
    }

    fn flush(&self) {}
}

/// Initialize the custom logger for testing.
pub(crate) fn init_logging() {
    // log::set_logger may only be called once, it returns an Err in subsequent
    // calls.
    if log::set_logger(&*TEST_LOGGER).is_ok() {
        log::set_max_level(LevelFilter::Trace);
    }
}

pub(crate) fn get_logs_for_current_thread() -> Vec<String> {
    TEST_LOGGER
        .messages
        .lock()
        .unwrap_or_else(|e| e.into_inner())
        .get(&std::thread::current().id())
        .cloned()
        .unwrap_or_default()
}
pub(crate) fn test_data(path: &str) -> String {
    format!("{}/tests/data/{}", env!("CARGO_MANIFEST_DIR"), path)
}

pub(crate) fn make_temporary_directory() -> TempDir {
    tempfile::tempdir().expect("the temporary directory could not be created")
}

pub(crate) fn rand_int(min: i32, max: i32) -> i32 {
    rand::rng().random_range(min..max)
}

pub(crate) fn rand_usize(min: usize, max: usize) -> usize {
    rand::rng().random_range(min..max)
}

pub(crate) fn rand_offset() -> (isize, isize) {
    (
        rand::rng().random_range(-1i8..=1) as isize,
        rand::rng().random_range(-1i8..=1) as isize,
    )
}

pub(crate) fn rand_float() -> f64 {
    rand::random()
}

pub(crate) fn rand_bool() -> bool {
    rand::random()
}

pub(crate) fn rand_string(length: usize) -> String {
    let charset = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789‐‑‒–—―‖‗\
                   ‘’‚‛“”„‟†‡•‣․‥…‧‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁄⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗─━│┃┄┅┆┇┈┉┊┋┌┍┎┏";
    random_string::generate(length, charset)
}

pub(crate) fn random_text_tile_content_vector() -> Vec<String> {
    //! Create a vector of random length with random strings of a uniform random
    //! length.
    random_text_tile_content_vector_of_size(rand_usize(1, 12), rand_usize(1, 19))
}

pub(crate) fn random_text_tile_content_vector_of_size(
    row_count: usize,
    col_count: usize,
) -> Vec<String> {
    std::iter::repeat_with(|| rand_string(col_count))
        .take(row_count)
        .collect()
}

pub(crate) fn random_connectors() -> Connectors {
    Connectors {
        north: rand_usize(1, 3),
        east: rand_usize(1, 3),
        south: rand_usize(1, 3),
        west: rand_usize(1, 3),
    }
}

pub(crate) fn random_tiles(count: usize) -> Vec<Tile> {
    std::iter::repeat_with(|| random_tile_text(random_connectors()))
        .take(count)
        .collect()
}

pub(crate) fn random_tile_text(connectors: Connectors) -> Tile {
    random_tile_text_with_weight(connectors, rand_float())
}

pub(crate) fn random_tile_text_with_weight(connectors: Connectors, weight: f64) -> Tile {
    Tile::new_text(
        rand_usize(67, 90),
        rand_string(rand_usize(0, 13)),
        random_text_tile_content_vector(),
        connectors,
        weight,
    )
    .expect("line lengths should match")
}

pub(crate) fn random_coordinates() -> (usize, usize) {
    (rand_usize(1, 6), rand_usize(1, 6))
}

pub(crate) fn random_predecessor() -> Option<(usize, usize)> {
    if rand_bool() {
        None
    } else {
        Some(random_coordinates())
    }
}

pub(crate) fn random_border_tile_id() -> Option<usize> {
    if rand_bool() {
        None
    } else {
        Some(rand_usize(0, 10))
    }
}

pub(crate) fn random_missing_tile_policy() -> MissingTilePolicy {
    if rand_bool() {
        MissingTilePolicy::Avoid
    } else {
        MissingTilePolicy::Exit
    }
}

pub(crate) fn random_tile_selection_policy() -> TileSelectionPolicy {
    if rand_bool() {
        TileSelectionPolicy::Priority
    } else {
        TileSelectionPolicy::Random
    }
}

pub(crate) fn random_vacancy_policy() -> VacancyPolicy {
    if rand_bool() {
        VacancyPolicy::Ignore
    } else if rand_bool() {
        VacancyPolicy::Avoid
    } else {
        VacancyPolicy::AvoidStrict
    }
}

pub(crate) fn random_tile_type() -> TileType {
    if rand_bool() {
        TileType::Raster
    } else if rand_bool() {
        TileType::Svg
    } else {
        TileType::Text
    }
}

pub(crate) fn random_direction() -> Direction {
    match rand_usize(0, 4) {
        0 => Direction::North,
        1 => Direction::East,
        2 => Direction::South,
        3 => Direction::West,
        _ => unreachable!(),
    }
}

pub(crate) fn random_option_direction() -> Option<Direction> {
    if rand_bool() {
        None
    } else {
        Some(random_direction())
    }
}

pub(crate) fn random_exposed_edges() -> Vec<Direction> {
    std::iter::repeat_with(random_direction)
        .take(rand_usize(0, 4 + 1))
        .collect()
}

pub(crate) fn random_excluded_connectors() -> HashSet<Connectors> {
    std::iter::repeat_with(random_connectors)
        .take(rand_usize(0, 13))
        .collect()
}

pub(crate) fn random_last_leaves() -> Vec<(usize, usize)> {
    std::iter::repeat_with(random_coordinates)
        .take(rand_usize(0, 11))
        .collect()
}

pub fn neighbor_association_tile_list() -> TileList {
    TileList {
        tiles: vec![
            // 0
            random_tile_text(Connectors {
                north: 1,
                east: 1,
                south: 1,
                west: 1,
            }),
            // 1
            random_tile_text(Connectors {
                north: rand_usize(2, 10),
                east: 1,
                south: 1,
                west: 1,
            }),
            // 2
            random_tile_text(Connectors {
                north: 1,
                east: rand_usize(2, 10),
                south: 1,
                west: 1,
            }),
            // 3
            random_tile_text(Connectors {
                north: 1,
                east: 1,
                south: rand_usize(2, 10),
                west: 1,
            }),
            // 4
            random_tile_text(Connectors {
                north: 1,
                east: 1,
                south: 1,
                west: rand_usize(2, 10),
            }),
            // 5
            random_tile_text(Connectors {
                north: rand_usize(2, 10),
                east: rand_usize(2, 10),
                south: rand_usize(2, 10),
                west: rand_usize(2, 10),
            }),
            // 6
            random_tile_text(Connectors {
                north: 1,
                east: rand_usize(2, 10),
                south: rand_usize(2, 10),
                west: rand_usize(2, 10),
            }),
            // 7
            random_tile_text(Connectors {
                north: rand_usize(2, 10),
                east: 1,
                south: rand_usize(2, 10),
                west: rand_usize(2, 10),
            }),
            // 8
            random_tile_text(Connectors {
                north: rand_usize(2, 10),
                east: rand_usize(2, 10),
                south: 1,
                west: rand_usize(2, 10),
            }),
            // 9
            random_tile_text(Connectors {
                north: rand_usize(2, 10),
                east: rand_usize(2, 10),
                south: rand_usize(2, 10),
                west: 1,
            }),
        ],
        horizontal_connector_names: vec![],
        vertical_connector_names: vec![],
        tile_type: random_tile_type(),
    }
}
